HTTP : Cookie는 어떻게 돌아다니나?

HTTP 쿠키는 서버가 사용자의 웹 브라우저에 전송하는 데이터 조각으로, 브라우저는 그 데이터 조각들을 저장해 놓았다가, 동일한 Origin에 재 요청 시 저장된 쿠키를 함께 전송한다. 상태가 없는(stateless) HTTP 프로토콜에서 상태 정보를 기억시켜주기 때문에 세션관리, 개인화, 트래킹 용도로 많이 사용된다.

쿠키 만들기 : Set-Cookie와 Cookie

서버는 응답과 함께 Set-Cookie 헤더를 전송할 수 있다. 쿠키는 브라우저에 저장되며, 그 후 쿠키는 같은 Origin로의 요청(Request)의 Cookie 헤더안에 포함되어 전송된다.

  1. 서버는 어떤 요청이라도 응답 헤더에 Set-Cookie : <name>=<value> 형태를 담을 수 있으며, Set-Cookie 헤더는 여러개일 수 있다.
    • 만료일 혹은 지속시간(duration)도 명시될 수 있고, 만료된 쿠키는 더이상 보내지지 않는다.
    • 특정 도메인 혹은 경로 제한을 설정할 수 있으며 이는 쿠키가 보내지는 것을 제한할 수 있다.
  2. 브라우저는 쿠키를 Origin에 따라 저장해두고, 똑같은 Origin에 대한 요청이면 Cookie헤더에 Set-Cookie가 전달해준 내용들을 담아보낸다.

쿠키 공유 범위 조절하기 : Domain 과 Path 디렉티브

Origin은 리소스의 출처를 말하는데, 프로토콜 + 호스트 + 포트의 조합으로 식별된다. 기본적으로 브라우저는 교차 출처 (다른 Origin)끼리 리소스 공유를 못하게 막는다. 이것이 SOP 정책이고, 교차 출처간 요청을 허용하는 정책이 CORS 정책이다.

웹서비스에서 www.naver.com의 HTML 응답에 서버는 Set-Cookie를 담을 수 있고api.naver.com/news 라는 API 응답에도 서버는 Set-Cookie 헤더를 담을 수 있지만 둘은 Origin이 다르기 때문에, 둘의 Cookie는 공유되지 않는다. (쿠키가 요청에 따라가지 않는다)

Domain 디렉티브

기본적으로 교차 출처간 쿠키는 공유되지 않는다. 그러나 Domain 디렉티브를 사용하면 서브도메인 간 공유가 가능하다.
Set-Cookie : id=123; Domain=.naver.com 으로 헤더를 설정해두면 api.naver.comwww.naver.com 요청에 대해 "id"라는 쿠키가 따라간다.(공유된다.)

Path 디렉티브

Path 디렉티브로 Path별로 쿠키 공유 범위를 제한할 수 있다.

Set-Cookie : id=123; Path=/news로 헤더를 설정하면 www.naver.com/newwww.naver.com/news/123는 쿠키가 공유된다. 하지만 www.naver.com와는 공유되지 않는 쿠키가 된다.


보안을 위한 디렉티브 SameSite, Secure, HttpOnly

XSS 공격과 HttpOnly 디렉티브

XSS(Cross Site Scripting) 공격이란, 자바스크립트 코드를 통해서 브라우저 저장소를 읽어 중요 정보를 탈취하는 것을 의미한다. 아래와 같은 게시글을 작성해서 다른 유저들이 읽으면 정보를 탈취하는 것이 XSS 공격이다.

<script>
  {
    나에게전송(document.cookie);
  }
</script>

Set-Cookie: session=abc123; HttpOnly

HttpOnly가 붙은 쿠키는 JS에서 document.cookie로 읽거나 조작 불가능하다. 즉, 브라우저 내부에서만 사용되고, 오직 HTTP 요청 시 자동 전송만 가능하다. 대신 클라이언트 JS코드에서도 접근 불가능해서 토큰을 관리하는 것이 힘들어진다.

하지만, HttpOnly를 쓴다고 해서 XSS를 완전히 막을 수 있는 것은 아니니 SanitizeDOMPurify같은 라이브러리를 통해, <script>, onerror, onclick, javascript: 같은 악성 태그나 이벤트 속성을 제거해버리는 것도 방법이다.

CSRF 공격을 막기 위한 SameSite 디렉티브

CSRF(Cross-Site Request Forgery)는 말 그대로 다른 사이트에서 “위조된 요청”을 보냄으로써 사용자의 의도와 상관없이 어떤 행동을 수행하게 하는 공격이다. 어떤 유저가 real.com에 로그인되어 있을 때, 공격자가 만든 fake.com에 사용자를 방문해서 아래와 같은 컴포넌트를 만나게되면

<img src="https://fake.com/sendDollar?to=사기꾼&amount=100000" />

브라우저는 동일한 Origin에 대한 요청이라 판단하여 real.com의 인증토큰도 함께 보내서, 의도치않게 돈을 송금하게 만들 수 있다. 즉 CSRF는 브라우저의 편의성(자동 쿠키 전송)을 노린 것이다.

Set-Cookie: session=abc123; SameSite=<값> SameSite 디렉티브를 넣어서 어느정도 보안을 유지할 수 있다. 이 속성은 “다른 사이트(Origin)에서 시작된 요청일 때, 이 쿠키를 자동으로 전송할지 여부”를 결정한다.

  • SameSite=Strict : 다른 사이트에서 들어온 모든 요청엔 쿠키 안 붙임
  • SameSite=Lax : Get 링크, 리다이렉트만 허용한다.
  • SameSite=None : 크로스사이트 허용한다.

중간자 공격과 Secure

웹 통신은 기본적으로 클라이언트 ↔ 서버 사이에 네트워크를 통해 전달된다. 이 사이에서 공격자가 패킷을 가로채거나 조작하는 것이 중간자 공격(Man-In-The-Middle, MITM) 이다. 쿠키가 평문(HTTP)으로 전송되면, 네트워크에서 쿠키 값을 읽어 세션을 탈취하거나 요청을 위조할 수 있다

Set-Cookie: session=abc123; Secure; Secure 디렉티브는 쿠키가 오직 HTTPS 연결에서만 브라우저에 의해 전송되도록 강제하는 속성이다.

  • HTTPS는 트래픽을 TLS 암호화로 보호한다. 암호화가 있으면 중간자가 패킷 내용을 읽을 수 없고, 수정도 어렵다(무결성).
  • 단, Secure만 붙인다고 끝나는 게 아니다 — 서버와 클라이언트 전체가 TLS를 제대로 적용해야 동작한다.

인증토큰을 쿠키를 통해 보내야하나 Header를 사용해야하나

인증 토큰을 Cookie에 담는 것이 아닌 Authorization Header에 담는 방식도 있다.

HttpOnly, SameSite, Secure등 쿠키 설정은 복잡하고, 서브 도메인이 아닌 경우에 이 쿠키를 공유하기 어렵기 때문이다. Header는 클라이언트에서 직접 로직을 통해 삽입 및 제어할 수 있기 때문에 범용성이 있다.


교차 Origin 간에 Cookie를 공유하기 위해 해야될 일

Access-Control-Allow-Credentials:true

Domain 디렉티브를 통해 교차 출처에 대해서 쿠키 공유가 가능하다고했다.

일단, 먼저 알아야하는 것은 1차 도메인이 다르면 절대 공유가 안된다는 점이다 (RFC6265) Domain 디렉티브를 보면 알 수 있듯, 오직 서브도메인까지만 허용된다.

브라우저에서 교차 출처 요청에 대해선, HTTPS와 CORS정책에 의해 막히는데 교차 출처간 쿠키를 공유하려면 다음과 같은 조건이 붙어야한다.

  1. Set-Cookie 헤더의 Domain 디렉티브 를 삽입한다.
  2. Set-Cookie 헤더의 디렉티브가 SameSite=Strict가 아니어야한다.
    1. 다만, Lax는 Get요청만 허용하므로, 일반적으로 사용하려면 SameSite=None 이어야 한다.
  3. Set-Cookie 헤더의 디렉티브에 Secure 가 있어야한다. (조건부)
    1. SameSite=None이 동작하려면 Secure가 필수기 때문이다.
  4. 2~3번 조건을 만족하려면 결국, 교차출처 모두 HTTPS(TLS) 인증을 받아둬야한다.
    1. 뿐만 아니라, HTTPS가 아니라면 교차출처는 CORS이전에 막힌다.
  5. Set-Cookie가 있는 응답 헤더에 Access-Control-Allow-Credentials: true 담아야한다.
    1. 이때, Access-Control-Allow-Origin*와일드카드일 때는 동작하지 않는다. 즉 교차 출처 범위가 와일드카드면 위의 Credentials는 동작하지 않는다.
  6. 요청을 보낼때, fetch('/api',{credentials : 'include'}) 처럼 쿠키를 명시해야만 한다.

아주 복잡하다.

그래서 MSA나 서브도메인으로 구축된 서비스에서는 저 복잡한 정책을 맞춰 쿠키를 공유하거나 아니면 Authorization 헤더로 전달하도록 만드는 경우로 나뉜다.


Reference